/**
 * @class 相对变换摄像机组件
 * @author DuskyHuang 圣子
 * @description 此类的作用：将相机停留在半径为R的空间球上旋转拍摄，让世界产生相对变换，将视点聚焦球体内以规避无穷远处精读丢失。
 * @description 此类还提供：触摸热区设置、鼠标/键盘灵敏度设置，暴露镜头控制的函数调用等功能。
*/

import { _decorator, Component, Vec2, input, sys, Node, Input, EventTouch, EventMouse, view, Rect, Vec3, Quat, IVec3Like, UITransform, KeyCode, EventKeyboard } from 'cc';
import { EDITOR } from 'cc/env';
import { core, utils } from 'cfw';
const { serialize, group, tip } = utils;
const { ccclass } = _decorator;
const showUseArea: Validator = function (this: RelativeCamera) { return !this.toucher; }
const showHotArea: Validator = function (this: RelativeCamera) { return !this.toucher && this.useHotArea; }
const noScrollingStick: Validator = function (this: RelativeCamera) { return this.noScrollingStick; }

@ccclass('cocosmos.relative-camera')
export class RelativeCamera extends Component {

    @tip("使用节点对象", UITransform) public toucher: UITransform = null;
    @tip("使用热区", showUseArea) public useHotArea: boolean = false;
    @tip('热区范围', showHotArea) public hotArea: Rect = new Rect(0, 0, 1, 1);

    @serialize private _faceAngles: FaceAngles = { az: 0, ha: 0 };
    @serialize private _R: number = 1;
    @group("Geometry", "球半径", [0, 100, 0.01])
    public get fixedSphereRadius(): number { return this._R; }
    public set fixedSphereRadius(arg: number) { this._R = arg; this._adjustFaceAngles(); }

    @group("Geometry", "高度角（°）", [-89, 89, 0.01])
    public get heightAngle(): num.deg { return this._faceAngles.ha; }
    public set heightAngle(arg: num.deg) { this._faceAngles.ha = arg; if (EDITOR) this._adjustFaceAngles(); }
    @group("Geometry", "方向角（°/+Z）", [-180, 180, 0.01])
    public get azimuthAngle(): num.deg { return this._faceAngles.az; }
    public set azimuthAngle(arg: num.deg) { this._faceAngles.az = arg; if (EDITOR) this._adjustFaceAngles(); }


    @group("Interactive", "是否可交互")
    public interactable: boolean = true;

    @group("Interactive", "PC端启用键盘")
    public keyEnabled: boolean = true;

    @group("Interactive", "水平灵敏率", [0, 1, 0.01]) public horSensitivity: num.pct = 0.5;
    @group("Interactive", "竖直灵敏率", [0, 1, 0.01]) public verSensitivity: num.pct = 0.5;
    @serialize private _ss: num.pct = 0.93303299;
    @group("Interactive", "滚动缓动率", [0, 1, 0.01])
    public get scrollSmoothing(): num.pct { return Math.pow(this._ss, 10); }
    public set scrollSmoothing(arg: num.pct) { this._ss = Math.pow(arg, 0.1); }

    @group("Interactive", "缩放灵敏度", [0, 1, 0.01])
    public zoomSensitivity: number = 0.5;
    @group("Interactive", "滚轮灵敏度", [0, 1, 0.01])
    public wheelSensitivity: number = 0.5;
    @group("Interactive", "反向缩放操作")
    public zoomReversed: boolean = false;
    @group("Interactive", "截停滚动粘滞")
    public noScrollingStick: boolean = false;
    @group("Interactive", "截停粘滞阈值（°/s）", noScrollingStick, [0.1, 10, 0.1])
    public noScrollingValveAngle: number = 1;

    @serialize private _zoomNotify: string = 'Relative.Zooming';
    @group("Interactive", "缩放通知", [0, 1, 0.01])
    public get zoomNotify(): string { return this._zoomNotify; }
    public set zoomNotify(arg: string) { if (arg) this._zoomNotify = arg; }

    /**
     * 获取当前的镜头旋转角速度（单位：角度/秒）
     * @notice 这个值是球面角，每帧lateUpdate时均会被更新
     */
    public get angularVelocity(): num.deg {
        return this._angular;
    }

    /**
     * 开始镜头自动旋转
     * @param azPs 每秒钟方向角旋转的度数，设置为负值为反向旋转 eg.180=每秒半圈
     * @param haPs 每秒钟高度角抬升的度数，设置为负值为降低角度
     * @param rangeAz 水平方向角来回摆动的幅度范围
     * @param rangeHa 垂直高度角上下浮动的幅度范围 
     * @notice 自动旋转镜头时亦能交互，如果你希望自旋时镜头被锁定交互，同时设置.interactable = false 即可
     */
    public startAutoRotating(azPs: num.deg, haPs: num.deg, rangeAz?: vec2<num.deg>, rangeHa?: vec2<num.deg>): void {
        this._autoAccAz = azPs;
        this._autoAccHa = haPs;
        this._autoAzRange = rangeAz ?? null;
        this._autoHaRange = rangeHa ?? null;
    }

    public get autoAA2nd(): num.deg { return this._autoAA2nd; }
    public set autoAA2nd(arg: num.deg) { this._autoAA2nd = arg; }

    /**
      * 停止镜头自动旋转
      */
    public stopAutoRotating(): void {
        this._autoAccHa = this._autoAccAz = 0;
        this._autoAzRange = this._autoHaRange = null;
    }

    /**
     * 迫使镜头开始转向目标
     * @param faceTarget 面向的目标节点（方式1）
     * @param worldPosition 面向的世界坐标（方式2）
     * @param duration 此过程花费的时间，默认1秒
     * @notice 在此过程结束前，交互和自旋无效。
     */
    public async facingTo(faceTarget: Node, duration?: time.sec, immediateCallback?: Handler): Promise<void>;
    public async facingTo(worldPosition: IVec3Like, duration?: time.sec, immediateCallback?: Handler): Promise<void>;
    public async facingTo(arg: Node | IVec3Like, duration: time.sec = 1, cb?: Handler): Promise<void> {
        Quat.copy(this._from, this.node.worldRotation);
        Vec3.subtract(v3a, this.node.worldPosition, arg instanceof Node ? arg.worldPosition : arg);
        Quat.fromViewUp(this._to, Vec3.normalize(v3a, v3a));
        const { x, y, z } = v3a;
        this._faceAngles.ha = Math.parseDegree(Math.asin(y));
        this._faceAngles.az = Math.parseDegree(Math.atan2(x, z));
        this._duration = this._reset = Math.abs(duration);
        this._facing = true;
        this._angular = Quat.angle(this._from, this._to) / this._duration;
        return new Promise(resolve => this.scheduleOnce(() => {
            this.setFaceAngles(this._faceAngles);
            Function.invoke(cb);
            resolve();
            this._facing = false;
        }, duration));
    }
    private _facing: boolean = false;

    /**
     * 根据参数或目标节点立刻调整空间角
     * @param faceTarget 面向的世界坐标（方式1）
     * @param faceArgs 面向的空间角（方式2）
     * @notice 在此过程立刻生效，缓动数据将被重置。
     */
    public setFaceAngles(faceArgs: Partial<FaceAngles>): void;
    public setFaceAngles(faceTarget: Node): void;
    public setFaceAngles(args: Partial<FaceAngles> | Node): void {
        if (args instanceof Node) {
            Vec3.subtract(v3a, this.node.worldPosition, args.worldPosition).normalize();
            const { x, y, z } = Vec3.normalize(v3a, v3a);
            this._faceAngles.ha = Math.parseDegree(Math.atan(y));
            this._faceAngles.az = Math.parseDegree(Math.atan2(x, z));
        } else {
            if (isNum(args.az)) this._faceAngles.az = args.az;
            if (isNum(args.ha)) this._faceAngles.ha = args.ha;
        }
        this._adjustFaceAngles();
    }

    protected onLoad(): void { this._listen(true); }

    protected onDestroy(): void { this._listen(false); core.unload(this); }

    protected onEnable(): void { this._adjustFaceAngles(); }

    protected update(dt: time.sec): void {
        if (this._facing) {
            this._reset -= dt;
            const t = Math.clamp01(1 - this._reset / this._duration);
            Quat.slerp(quat, this._from, this._to, t);
            this.node.setWorldRotation(quat);
        } else {
            this._autoRotate(dt);
            const ratio = Math.lerp(1, dt, this._ss), { az, ha } = this._faceAngles;
            Quat.slerp(quat, this.node.worldRotation, Quat.fromEuler(quat, -ha, az, 0), ratio);
            Quat.toEuler(v3a, quat);
            this._angular = Quat.angle(this.node.worldRotation, Quat.fromEuler(quat, -ha, az, 0)) / dt;
            if (this.noScrollingStick && this._angular > this.noScrollingValveAngle) {
                Quat.slerp(quat, this.node.worldRotation, quat, ratio);
                Quat.toEuler(v3a, quat);
                this._adjustFaceAngles({ az: v3a.y, ha: -v3a.x });
            } else this._adjustFaceAngles({ az: v3a.y, ha: -v3a.x });
        }
    }

    private _autoRotate(dt: time.sec): void {
        this._faceAngles.az += (this._autoAccAz + this._autoAA2nd) * dt;
        this._faceAngles.ha += this._autoAccHa * dt;
        if (notEmpty(this._autoAzRange)) {
            const [min, max] = this._autoAzRange;
            if (this._faceAngles.az <= min) this._autoAccAz = Math.abs(this._autoAccAz);
            if (this._faceAngles.az >= max) this._autoAccAz = -Math.abs(this._autoAccAz);
        }
        if (notEmpty(this._autoHaRange)) {
            const [min, max] = this._autoHaRange;
            if (this._faceAngles.ha <= min) this._autoAccHa = Math.abs(this._autoAccHa);
            if (this._faceAngles.ha >= max) this._autoAccHa = -Math.abs(this._autoAccHa);
        }
    }

    protected onTouchStart(touch: EventTouch): void {
        this._checkInteractive(touch, () => {
            const touches = touch.getAllTouches();
            if (touches.length === 2) {
                const [t1, t2] = touches;
                dist = Vec2.distance(t1.getLocation(v2a), t2.getLocation(v2b));
            }
        });
    }

    protected onTouchMove(touch: EventTouch): void {
        this._checkInteractive(touch, () => {
            const touches = touch.getAllTouches();
            if (touches.length === 1) {
                this.scrolling(touch.getDeltaX(), touch.getDeltaY());
            } else if (touches.length === 2) {
                const [t1, t2] = touches;
                const { zoomSensitivity, zoomReversed, _zoomNotify } = this;
                const _dist = Vec2.distance(t1.getLocation(v2a), t2.getLocation(v2b)) + 0.0001;
                const delta = (_dist - dist) * zoomSensitivity * (zoomReversed ? -1 : 1) * 0.001;
                if (delta != 0.0001 && _zoomNotify) {
                    dist = _dist;
                    core.notify(_zoomNotify, delta);
                }
            }
        });
    }

    protected onMouseWheel(mouse: EventMouse): void {
        this._checkInteractive(mouse, () => {
            const { wheelSensitivity, _zoomNotify } = this;
            const delta = Math.sign(-mouse.getScrollY()) * wheelSensitivity * 0.1;
            if (delta != 0 && _zoomNotify) {
                core.notify(_zoomNotify, delta);
            }
        });
    }

    protected onMouseMove(mouse: EventMouse): void {
        this._checkInteractive(mouse, () => {
            const btn = mouse.getButton();
            if (btn === EventMouse.BUTTON_LEFT) {
                this.scrolling(mouse.getDeltaX(), mouse.getDeltaY());
            }
        });
    }

    protected scrolling(dx: number, dy: number): void {
        const { verSensitivity, horSensitivity } = this;
        this._faceAngles.az -= dx * Math.lerp(0, 0.2, horSensitivity);
        this._faceAngles.ha -= dy * Math.lerp(0, 0.1, verSensitivity);
        this._faceAngles.ha = Math.clamp(this._faceAngles.ha, -89, 89);
    }

    // 事件监听的注册和注销
    private _listen(arg: boolean): void {
        const host = this.toucher ? this.toucher.node : input;
        let callee = arg ? host.on : host.off;
        if (sys.isMobile) {
            Function.invoke(callee, host, Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
            Function.invoke(callee, host, Input.EventType.TOUCH_START, this.onTouchStart, this);
        } else {
            Function.invoke(callee, host, Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
            Function.invoke(callee, host, Input.EventType.MOUSE_WHEEL, this.onMouseWheel, this);
            if (this.keyEnabled) {
                callee = arg ? input.on : input.off;
                Function.invoke(callee, input, Input.EventType.KEY_DOWN, this.onKeyDown, this);
                Function.invoke(callee, input, Input.EventType.KEY_UP, this.onKeyUp, this);
            }
        }
    }

    protected onKeyDown(kb: EventKeyboard): void {
        this._pressingKeys.pushUnique(kb.keyCode);
    }

    protected onKeyUp(kb: EventKeyboard): void {
        this._pressingKeys.remove(kb.keyCode);
    }

    protected lateUpdate(): void {
        const pk = this._pressingKeys;
        let dx = 0, dy = 0, zm = 0;
        if (this.keyEnabled) {
            if (pk.contains(KEY_W) || pk.contains(ARROW_UP)) dy--;
            if (pk.contains(KEY_S) || pk.contains(ARROW_DOWN)) dy++;
            if (pk.contains(KEY_D) || pk.contains(ARROW_RIGHT)) dx--;
            if (pk.contains(KEY_A) || pk.contains(ARROW_LEFT)) dx++;
            if (pk.contains(KEY_E) || pk.contains(NUM_PLUS)) zm--;
            if (pk.contains(KEY_Q) || pk.contains(NUM_SUBTRACT)) zm++;
            if (dx + 0 || dy != 0) this.scrolling(dx * KEY_SCROLL_VS_TOUCH, dy * KEY_SCROLL_VS_TOUCH);
            const { wheelSensitivity, _zoomNotify } = this;
            if (zm != 0 && _zoomNotify) {
                core.notify(_zoomNotify, zm * wheelSensitivity * 0.1 * KEY_ZOOM_VS_TOUCH);
            }
        }
    }

    // 检测是否可交互（当前是交互状态且触点在热区内）
    private _checkInteractive(evt: EventTouch | EventMouse, next: Lambda): void {
        if (this.interactable) {
            if (!this.toucher && this.useHotArea) {
                const { x: px, y: py } = evt.getUILocation(v2a);
                const { width: W, height: H } = view.getVisibleSize();
                const { x, y, width: w, height: h } = this.hotArea;
                const [l, r, t, b] = [x * W, (x + w) * W, (1 - y) * H, (1 - y - h) * H];
                if (px >= l && px <= r && py <= t && py >= b) next();
            } else next();
        }
    }

    // 立刻在R球上调整面向角
    private _adjustFaceAngles(args?: FaceAngles): void {
        const { ha, az } = args ?? this._faceAngles;
        const [Ha, Az] = [Math.parseRadian(ha), Math.parseRadian(az)];
        const [uHor, uVer] = [Math.cos(Ha), Math.sin(Ha)];
        v3a.set(Math.sin(Az) * uHor, uVer, Math.cos(Az) * uHor);
        this.node.setWorldPosition(v3a.multiplyScalar(this._R));
        this.node.forward = Vec3.negate(v3a, v3a);
        /**
         * @reserved 以下代码是效果完全相同的另一种数学计算方式
         * const { ha, az } = args ?? this._stepAngles;
         * this.node.setWorldRotationFromEuler(-ha, az, 0);
         * const dir = Vec3.multiplyScalar(v3a, Vec3.UNIT_Z, this._R);
         * Vec3.rotateX(v3a, dir, Vec3.ZERO, Math.parseRadian(-ha));
         * Vec3.rotateY(v3a, v3a, Vec3.ZERO, Math.parseRadian(az));
         * this.node.setWorldPosition(v3a);
        */
    }

    private _angular: num.deg = 0;              // 角度变化
    private _duration: time.sec = 0;            // 转向目标总共时间
    private _reset: time.sec = 0;               // 转向目标剩余时间
    private _from: Quat = new Quat;             // 转向目标起始方位 
    private _to: Quat = new Quat;               // 转向目标目标方位

    private _autoAccAz: number = 0;             // 自动旋转方向角速度
    private _autoAA2nd: number = 0;             // 自动旋转方向角速度（第二套独立）
    private _autoAccHa: number = 0;             // 自动旋转高度角速度
    private _autoAzRange: vec2<num.deg> = null; // 自动旋转方向角范围（产生左右摆动）
    private _autoHaRange: vec2<num.deg> = null; // 自动旋转高度角范围（产生上下浮动）

    private _pressingKeys: KeyCode[] = [];      // 按下键位
}

// 空间角（=水平方向角+垂直高度角）
interface FaceAngles { az: num.deg, ha: num.deg };
let dist = 0;
const v2a = new Vec2;
const v2b = new Vec2;
const v3a = new Vec3;
const quat = new Quat;

const {
    KEY_W, KEY_S, KEY_A, KEY_D,
    ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT,
    KEY_Q, KEY_E, NUM_PLUS, NUM_SUBTRACT
} = KeyCode;

const KEY_SCROLL_VS_TOUCH: num.ratio = 5;
const KEY_ZOOM_VS_TOUCH: num.ratio = 0.2;